
import java.util.*;
import javax.swing.table.*;


public class Search
{
    private final List workers = new ArrayList();
    private final List hits = new ArrayList();
    private final WorkersTableModel workersTableModel = new WorkersTableModel();
    private final HitsTableModel hitsTableModel = new HitsTableModel();
    private final String query;
    private long startTime = -1;
    private long stopTime = -1;


    Search(String query) {
	super();
	this.query = query;
	workers.add(new ExciteSearchWorker(this, query));
	workers.add(new AltaVistaSearchWorker(this, query));
	workers.add(new LycosSearchWorker(this, query));
	workers.add(new GoogleSearchWorker(this, query));
    }


    /** 
     * @return the query we've passed to the SearchWorkers
     */
    public String getQuery() {
	return query;
    }

    
    /**
     * @return a read-only view of the list of SearchWorkers
     */
    public List getWorkers() {
	return new AbstractList() {
	    public Object get(int i) {
		return workers.get(i);
	    }
	    public int size() {
		return workers.size();
	    }
	};
    }


    /**
     * @return a read-only view of the list of SearchHits
     */
    public List getHits() {
	return new AbstractList() {
	    public Object get(int i) {
		return hits.get(i);
	    }
	    public int size() {
		return hits.size();
	    }
	};
    }

    
    /** 
     * Spawn a SearchWorker thread for each known search engine and start 
     * collecting SearchHits for the specified query.
     */
    public void start() {
	startTime = System.currentTimeMillis();
	ListIterator workersItr = workers.listIterator();
	while(workersItr.hasNext()) {
	    SearchWorker worker = (SearchWorker)(workersItr.next());
	    worker.start();
	}
    }


    /**
     * Abort the search - interrupt all of the SearchWorker threads that 
     * are still active.
     */
    public void stop() {
	ListIterator workersItr = workers.listIterator();
	while(workersItr.hasNext()) {
	    SearchWorker worker = (SearchWorker)(workersItr.next());
	    worker.interrupt();
	}
	stopTime = System.currentTimeMillis();
    }

    
    /** 
     * Return the number of milliseconds since this Search was started, 0
     * if the search hasn't started yet.  If the search has finished
     * then report the total elapsed time.
     */
    public long getElapsedTime() {
	if (startTime == -1) {
	    return 0L;
	}
	if (stopTime != -1) {
	    return Math.max(0L, stopTime - startTime);
	}
	else {
	    return Math.max(0L, System.currentTimeMillis() - startTime);
	}
    }


    /**
     * This method is effectively called by the worker threads when they
     * finish normally.  If all the threads have finished (or have been 
     * interrupted or aborted) we set stopTime here.
     */
    public void finished(SearchWorker finishedWorker) {
	ListIterator workersItr = workers.listIterator();
	boolean allWorkersFinished = true;
	while(workersItr.hasNext() && allWorkersFinished) {
	    SearchWorker worker = (SearchWorker)(workersItr.next());
	    allWorkersFinished = (worker.getStatus() > SwingWorker.RUNNING);
	}
	if (allWorkersFinished) {
	    stopTime = System.currentTimeMillis();
	}
    }


    /** 
     * This method is effectively called by the worker threads after
     * new SearchHits have been found and an update has been asked for.
     * It's not actually called on the worker thread - it's called by 
     * Runnable that's queued up by the worker thread with invokeLater().
     */
    public void update(List newHits) {
	int oldHitCount = hits.size();
	hits.addAll(newHits);
	int newHitCount = hits.size();
	if (newHitCount > oldHitCount) {
	    hitsTableModel.fireTableRowsInserted(oldHitCount, newHitCount - 1);
	    workersTableModel.fireTableDataChanged();
	}
    }


    /**
     * An instance of this class is used by a Comparator object
     * which in turn is used to sort the list of SearchHits or SearchWorkers.
     * The get method returns the property of the object the comparator
     * will use to define the sort order.
     */
    private static abstract class GetComparable {
	abstract Comparable get(Object o);
    }


    /** 
     * A Comparator that uses a GetComparable instance to extract a 
     * property from the pair of objects passed to it's compare method.
     * In other words, this is a Comparator that can be configured to 
     * compare some property of a pair of objects, instead of the objects
     * themselves.
     */
    private static class PropertyComparator implements Comparator {
	private final GetComparable getComparable;
	private final boolean ascending;
	PropertyComparator(GetComparable getComparable, boolean ascending) {
	    this.getComparable = getComparable;
	    this.ascending = ascending;
	}
	public int compare(Object o1, Object o2) {
	    if (ascending) {
		return getComparable.get(o1).compareTo(getComparable.get(o2));
	    }
	    else {
		return getComparable.get(o2).compareTo(getComparable.get(o1));
	    }
	}
	public boolean equals(Object o) {
	    return super.equals(o);
	}
    }



    /** 
     * A TableModel view of the list of SearchWorkers.  The columns
     * are organized as follows:
     * 0 - search engine
     * 1 - worker status
     * 2 - number of hits found
     * 3 - elapsed time 
     */
    private class WorkersTableModel extends AbstractTableModel 
    {
	private String[] columnNames = {"Source", "Status", "Hits", "Idle Time (ms)"};

	public int getRowCount() {
	    return workers.size();
	}
	
	public int getColumnCount() {
	    return columnNames.length;
	}

	public String getColumnName(int col) {
	    return columnNames[col];
	}

	public Class getColumnClass(int col) {
	    return String.class;
	}

	private String getWorkerStatus(SearchWorker worker) {
	    switch (worker.getStatus()) {
	    case SearchWorker.READY: return "Ready";
	    case SearchWorker.RUNNING: return "Running";
	    case SearchWorker.INTERRUPTED: return "Interrupted";
	    case SearchWorker.FINISHED: return "Finished";
	    case SearchWorker.ABORTED: return "Aborted";
	    default:
		return "<unknown status>";
	    }
	}

	public Object getValueAt(int row, int col) {
	    SearchWorker worker = (SearchWorker)(workers.get(row));
	    switch(col) {
	    case 0: return worker.getSiteName();
	    case 1: return getWorkerStatus(worker);
	    case 2: return Integer.toString(worker.getNHits());
	    case 3: return Long.toString(worker.getReadIdleTime());
	    default:
		throw new ArrayIndexOutOfBoundsException(col);
	    }
	}
    }

    public TableModel getWorkersTableModel() {
	return workersTableModel;
    }


    /** 
     * A TableModel view of the list of SearchHits.  The columns
     * are organized as follows:
     * <pre>
     * 0 - search engine
     * 1 - hostname of the hit URL
     * 2 - title of the hit URL contents
     * </pre>
     * The column ID objects are the sort keys for the column.  
     * @see #sortHits
     * @see TableColumn#getIdentifier
     */
    private class HitsTableModel extends AbstractTableModel 
    {
	private String[] columnNames = {"Source", "Host", "Title"};

	public int getRowCount() {
	    return hits.size();
	}
	
	public int getColumnCount() {
	    return columnNames.length;
	}

	public String getColumnName(int col) {
	    return columnNames[col];
	}

	public Class getColumnClass(int col) {
	    return (col == 0) ? javax.swing.Icon.class : String.class;
	}

	public Object getValueAt(int row, int col) {
	    SearchHit hit = (SearchHit)(hits.get(row));
	    switch(col) {
	    case 0: return hit.getWorker().getSiteIcon();
	    case 1: return hit.getURL().getHost();
	    case 2: return hit.getTitle();
	    default:
		throw new ArrayIndexOutOfBoundsException(col);
	    }
	}
    }


    public TableModel getHitsTableModel() {
	return hitsTableModel;
    }

    /**
     * A GetComparable object for each column in the SearchHits table model. 
     */
    private static final GetComparable[] sortHitsKeys = {
	new GetComparable() {
	    Comparable get(Object o) {
		return ((SearchHit)o).getWorker().getSiteName();
	    }
	},
	new GetComparable() {
	    Comparable get(Object o) {
		java.net.URL url = ((SearchHit)o).getURL();
		return (url != null) ? url.getHost() : "<couldn't parse URL>";
	    }
	},
	new GetComparable() {
	    Comparable get(Object o) {
		return ((SearchHit)o).getTitle();
	    }
	}
    };


    /**
     * Sort the SearchHits List using the specified column as the primary sort key. 
     */
    public void sortHits(int modelColIndex, boolean ascending) {
	GetComparable key = sortHitsKeys[modelColIndex];
	Collections.sort(hits, new PropertyComparator(key, ascending));
	hitsTableModel.fireTableDataChanged();
    }
}
